// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

/// <summary>
///     Relational database specific extension methods for LINQ queries.
/// </summary>
public static class RelationalQueryableExtensions
{
    /// <summary>
    ///     Creates a <see cref="DbCommand" /> set up to execute this query.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         This is only typically supported by queries generated by Entity Framework Core.
    ///     </para>
    ///     <para>
    ///         Warning: there is no guarantee that executing this command directly will result in the same behavior as if EF Core had
    ///         executed the command.
    ///     </para>
    ///     <para>
    ///         Note that DbCommand is an <see cref="IDisposable" /> object. The caller is responsible for disposing the returned
    ///         command.
    ///     </para>
    ///     <para>
    ///         This is only typically supported by queries generated by Entity Framework Core.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-diagnostics">Logging, events, and diagnostics</see> for more information and examples.
    ///     </para>
    /// </remarks>
    /// <param name="source">The query source.</param>
    /// <returns>The query string for debugging.</returns>
    public static DbCommand CreateDbCommand(this IQueryable source)
    {
        if (source.Provider.Execute<IEnumerable>(source.Expression) is IRelationalQueryingEnumerable queryingEnumerable)
        {
            return queryingEnumerable.CreateDbCommand();
        }

        throw new NotSupportedException(RelationalStrings.NoDbCommand);
    }

    #region FromSql

    /// <summary>
    ///     Creates a LINQ query based on a raw SQL query.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         If the database provider supports composing on the supplied SQL, you can compose on top of the raw SQL query using
    ///         LINQ operators: <c>context.Blogs.FromSqlRaw("SELECT * FROM Blogs").OrderBy(b => b.Name)</c>.
    ///     </para>
    ///     <para>
    ///         As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
    ///         attack. You can include parameter place holders in the SQL query string and then supply parameter values as additional
    ///         arguments. Any parameter values you supply will automatically be converted to a <see cref="DbParameter" />.
    ///     </para>
    ///     <para>
    ///         However, <b>never</b> pass a concatenated or interpolated string (<c>$""</c>) with non-validated user-provided values
    ///         into this method. Doing so may expose your application to SQL injection attacks. To use the interpolated string syntax,
    ///         consider using <see cref="FromSql{TEntity}" /> to create parameters.
    ///     </para>
    ///     <para>
    ///         This overload also accepts <see cref="DbParameter" /> instances as parameter values. In addition to using positional
    ///         placeholders as above (<c>{0}</c>), you can also use named placeholders directly in the SQL query string.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
    ///         for more information and examples.
    ///     </para>
    /// </remarks>
    /// <typeparam name="TEntity">The type of the elements of <paramref name="source" />.</typeparam>
    /// <param name="source">
    ///     An <see cref="IQueryable{T}" /> to use as the base of the raw SQL query (typically a <see cref="DbSet{TEntity}" />).
    /// </param>
    /// <param name="sql">The raw SQL query.</param>
    /// <param name="parameters">The values to be assigned to parameters.</param>
    /// <returns>An <see cref="IQueryable{T}" /> representing the raw SQL query.</returns>
    [StringFormatMethod("sql")]
    public static IQueryable<TEntity> FromSqlRaw<TEntity>(
        this DbSet<TEntity> source,
        [NotParameterized] string sql,
        params object[] parameters)
        where TEntity : class
    {
        Check.NotEmpty(sql, nameof(sql));
        Check.NotNull(parameters, nameof(parameters));

        var queryableSource = (IQueryable)source;
        return queryableSource.Provider.CreateQuery<TEntity>(
            GenerateFromSqlQueryRoot(
                queryableSource,
                sql,
                parameters));
    }

    /// <summary>
    ///     Creates a LINQ query based on an interpolated string representing a SQL query.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         If the database provider supports composing on the supplied SQL, you can compose on top of the raw SQL query using
    ///         LINQ operators.
    ///     </para>
    ///     <para>
    ///         As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
    ///         attack. You can include interpolated parameter place holders in the SQL query string. Any interpolated parameter values
    ///         you supply will automatically be converted to a <see cref="DbParameter" />.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
    ///         for more information and examples.
    ///     </para>
    /// </remarks>
    /// <typeparam name="TEntity">The type of the elements of <paramref name="source" />.</typeparam>
    /// <param name="source">
    ///     An <see cref="IQueryable{T}" /> to use as the base of the interpolated string SQL query (typically a <see cref="DbSet{TEntity}" />).
    /// </param>
    /// <param name="sql">The interpolated string representing a SQL query with parameters.</param>
    /// <returns>An <see cref="IQueryable{T}" /> representing the interpolated string SQL query.</returns>
    public static IQueryable<TEntity> FromSqlInterpolated<TEntity>(
        this DbSet<TEntity> source,
        [NotParameterized] FormattableString sql)
        where TEntity : class
    {
        Check.NotNull(sql, nameof(sql));
        Check.NotEmpty(sql.Format, nameof(source));

        var queryableSource = (IQueryable)source;
        return queryableSource.Provider.CreateQuery<TEntity>(
            GenerateFromSqlQueryRoot(
                queryableSource,
                sql.Format,
                sql.GetArguments()));
    }

    /// <summary>
    ///     Creates a LINQ query based on an interpolated string representing a SQL query.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         If the database provider supports composing on the supplied SQL, you can compose on top of the raw SQL query using
    ///         LINQ operators.
    ///     </para>
    ///     <para>
    ///         As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
    ///         attack. You can include interpolated parameter place holders in the SQL query string. Any interpolated parameter values
    ///         you supply will automatically be converted to a <see cref="DbParameter" />.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-raw-sql">Executing raw SQL commands with EF Core</see>
    ///         for more information and examples.
    ///     </para>
    /// </remarks>
    /// <typeparam name="TEntity">The type of the elements of <paramref name="source" />.</typeparam>
    /// <param name="source">
    ///     An <see cref="IQueryable{T}" /> to use as the base of the interpolated string SQL query (typically a <see cref="DbSet{TEntity}" />).
    /// </param>
    /// <param name="sql">The interpolated string representing a SQL query with parameters.</param>
    /// <returns>An <see cref="IQueryable{T}" /> representing the interpolated string SQL query.</returns>
    public static IQueryable<TEntity> FromSql<TEntity>(
        this DbSet<TEntity> source,
        [NotParameterized] FormattableString sql)
        where TEntity : class
    {
        Check.NotNull(sql, nameof(sql));
        Check.NotEmpty(sql.Format, nameof(source));

        var queryableSource = (IQueryable)source;
        return queryableSource.Provider.CreateQuery<TEntity>(
            GenerateFromSqlQueryRoot(
                queryableSource,
                sql.Format,
                sql.GetArguments()));
    }

    private static FromSqlQueryRootExpression GenerateFromSqlQueryRoot(
        IQueryable source,
        string sql,
        object?[] arguments,
        [CallerMemberName] string memberName = null!)
    {
        var entityQueryRootExpression = (EntityQueryRootExpression)source.Expression;

        var entityType = entityQueryRootExpression.EntityType;
        if ((entityType.BaseType != null || entityType.GetDirectlyDerivedTypes().Any())
            && entityType.FindDiscriminatorProperty() == null)
        {
            throw new InvalidOperationException(RelationalStrings.MethodOnNonTphRootNotSupported(memberName, entityType.DisplayName()));
        }

        return new FromSqlQueryRootExpression(
            entityQueryRootExpression.QueryProvider!,
            entityType,
            sql,
            Expression.Constant(arguments));
    }

    #endregion

    #region SplitQuery

    /// <summary>
    ///     Returns a new query which is configured to load the collections in the query results in a single database query.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         This behavior generally guarantees result consistency in the face of concurrent updates
    ///         (but details may vary based on the database and transaction isolation level in use).
    ///         However, this can cause performance issues when the query loads multiple related collections.
    ///     </para>
    ///     <para>
    ///         The default query splitting behavior for queries can be controlled by
    ///         <see cref="RelationalDbContextOptionsBuilder{TBuilder,TExtension}.UseQuerySplittingBehavior(QuerySplittingBehavior)" />.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-split-queries">EF Core split queries</see> for more information and examples.
    ///     </para>
    /// </remarks>
    /// <typeparam name="TEntity">The type of entity being queried.</typeparam>
    /// <param name="source">The source query.</param>
    /// <returns>A new query where collections will be loaded through single database query.</returns>
    public static IQueryable<TEntity> AsSingleQuery<TEntity>(
        this IQueryable<TEntity> source)
        where TEntity : class
        => source.Provider is EntityQueryProvider
            ? source.Provider.CreateQuery<TEntity>(
                Expression.Call(AsSingleQueryMethodInfo.MakeGenericMethod(typeof(TEntity)), source.Expression))
            : source;

    internal static readonly MethodInfo AsSingleQueryMethodInfo
        = typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(AsSingleQuery))!;

    /// <summary>
    ///     Returns a new query which is configured to load the collections in the query results through separate database queries.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         This behavior can significantly improve performance when the query loads multiple collections.
    ///         However, since separate queries are used, this can result in inconsistent results when concurrent updates occur.
    ///         Serializable or snapshot transactions can be used to mitigate this
    ///         and achieve consistency with split queries, but that may bring other performance costs and behavioral difference.
    ///     </para>
    ///     <para>
    ///         The default query splitting behavior for queries can be controlled by
    ///         <see cref="RelationalDbContextOptionsBuilder{TBuilder, TExtension}.UseQuerySplittingBehavior(QuerySplittingBehavior)" />.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-split-queries">EF Core split queries</see> for more information and examples.
    ///     </para>
    /// </remarks>
    /// <typeparam name="TEntity">The type of entity being queried.</typeparam>
    /// <param name="source">The source query.</param>
    /// <returns>A new query where collections will be loaded through separate database queries.</returns>
    public static IQueryable<TEntity> AsSplitQuery<TEntity>(
        this IQueryable<TEntity> source)
        where TEntity : class
        => source.Provider is EntityQueryProvider
            ? source.Provider.CreateQuery<TEntity>(
                Expression.Call(AsSplitQueryMethodInfo.MakeGenericMethod(typeof(TEntity)), source.Expression))
            : source;

    internal static readonly MethodInfo AsSplitQueryMethodInfo
        = typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(AsSplitQuery))!;

    #endregion

    #region ExecuteDelete

    /// <summary>
    ///     Deletes all database rows for the entity instances which match the LINQ query from the database.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         This operation executes immediately against the database, rather than being deferred until
    ///         <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
    ///         entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
    ///         to reflect the changes.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
    ///         for more information and examples.
    ///     </para>
    /// </remarks>
    /// <param name="source">The source query.</param>
    /// <returns>The total number of rows deleted in the database.</returns>
    public static int ExecuteDelete<TSource>(this IQueryable<TSource> source)
        => source.Provider.Execute<int>(Expression.Call(ExecuteDeleteMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression));

    /// <summary>
    ///     Asynchronously deletes database rows for the entity instances which match the LINQ query from the database.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         This operation executes immediately against the database, rather than being deferred until
    ///         <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
    ///         entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
    ///         to reflect the changes.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
    ///         for more information and examples.
    ///     </para>
    /// </remarks>
    /// <param name="source">The source query.</param>
    /// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
    /// <returns>The total number of rows deleted in the database.</returns>
    public static Task<int> ExecuteDeleteAsync<TSource>(this IQueryable<TSource> source, CancellationToken cancellationToken = default)
        => source.Provider is IAsyncQueryProvider provider
            ? provider.ExecuteAsync<Task<int>>(
                Expression.Call(ExecuteDeleteMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression), cancellationToken)
            : throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync);

    internal static readonly MethodInfo ExecuteDeleteMethodInfo
        = typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(ExecuteDelete))!;

    #endregion

    #region ExecuteUpdate

    /// <summary>
    ///     Updates all database rows for the entity instances which match the LINQ query from the database.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         This operation executes immediately against the database, rather than being deferred until
    ///         <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
    ///         entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
    ///         to reflect the changes.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
    ///         for more information and examples.
    ///     </para>
    /// </remarks>
    /// <param name="source">The source query.</param>
    /// <param name="setPropertyCalls">A collection of set property statements specifying properties to update.</param>
    /// <returns>The total number of rows updated in the database.</returns>
    public static int ExecuteUpdate<TSource>(
        this IQueryable<TSource> source,
        Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> setPropertyCalls)
        => source.Provider.Execute<int>(
            Expression.Call(ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, setPropertyCalls));

    /// <summary>
    ///     Asynchronously updates database rows for the entity instances which match the LINQ query from the database.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         This operation executes immediately against the database, rather than being deferred until
    ///         <see cref="DbContext.SaveChanges()" /> is called. It also does not interact with the EF change tracker in any way:
    ///         entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated
    ///         to reflect the changes.
    ///     </para>
    ///     <para>
    ///         See <see href="https://aka.ms/efcore-docs-bulk-operations">Executing bulk operations with EF Core</see>
    ///         for more information and examples.
    ///     </para>
    /// </remarks>
    /// <param name="source">The source query.</param>
    /// <param name="setPropertyCalls">A collection of set property statements specifying properties to update.</param>
    /// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
    /// <returns>The total number of rows updated in the database.</returns>
    public static Task<int> ExecuteUpdateAsync<TSource>(
        this IQueryable<TSource> source,
        Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> setPropertyCalls,
        CancellationToken cancellationToken = default)
        => source.Provider is IAsyncQueryProvider provider
            ? provider.ExecuteAsync<Task<int>>(
                Expression.Call(
                    ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, setPropertyCalls), cancellationToken)
            : throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync);

    internal static readonly MethodInfo ExecuteUpdateMethodInfo
        = typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(ExecuteUpdate))!;

    #endregion
}
